Skip to content

feat: add GCP compliance scanning#20

Merged
kkmookhey merged 5 commits into
transilienceai:mainfrom
oswin-mako:fix/gcp-scan-reporting
May 14, 2026
Merged

feat: add GCP compliance scanning#20
kkmookhey merged 5 commits into
transilienceai:mainfrom
oswin-mako:fix/gcp-scan-reporting

Conversation

@oswin-mako

Copy link
Copy Markdown

Summary

  • Adds GCP compliance scanning modules for IAM, networking, storage, encryption, logging, compute, and Cloud Run, with CIS GCP mappings and Google provider remediation templates.
  • Hardens GCP scan behavior so domain/project runner failures and per-resource IAM/GCS API failures surface as NOT_ASSESSED instead of silent drops or false PASS results.
  • Adds GCP provider labels and CIS GCP controls to Markdown/HTML reports and dashboard finding details.
  • Updates README, TRUST, CHANGELOG, and doc-claim integrity tests for AWS + Azure + GCP coverage.

Verification

  • env PYTHONPATH=src uv run --no-project --with pytest --with pydantic --with jinja2 --with google-auth --with google-api-python-client --with google-cloud-storage pytest tests/test_gcp tests/test_reports/test_report_generation.py tests/test_integrity/test_doc_claims.py -q
    • 190 passed
  • env PYTHONPATH=src uv run --no-project --with ruff ruff check --select F
    • All checks passed
  • python3 -m compileall -q src/shasta tests/test_gcp tests/test_reports tests/test_integrity
  • git diff --check

Note: the full project-managed uv run path was not used locally because this environment is missing the native Cairo dependency required by xhtml2pdf/svglib/rlpycairo/pycairo.

@kkmookhey kkmookhey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @oswin-mako — really comprehensive coverage across IAM, networking, storage, encryption, logging, compute, and Cloud Run. To validate it end-to-end, I provisioned a real GCP test environment (Terraform module with BAD/COMPLIANT resource pairs across all 7 domains) and ran the scanner against it. Surfaced a few issues worth fixing before merge.

I've pushed a reference branch with the rebase + four follow-up commits at https://github.com/transilienceai/shasta/tree/review/pr-20-gcp-fixes — feel free to cherry-pick the commits you agree with, or write your own fix and we can drop the reference branch.

Rebase needed

The branch is currently DIRTY against main (PR #21 / voice-console landed and touched README.md, TRUST.md, CHANGELOG.md). Resolution preserves both your numeric bumps and the voice-console wording. Also: rebasing onto current main makes tests/test_integrity/test_doc_claims.py::test_root_trust_test_count fail because the doc says 720 tests and the rebased tree collects 839. Both fixes are in commit 8c22945 on the reference branch.

Bug 1 — KMS location wildcard returns 404

src/shasta/gcp/encryption.py:56 calls

kms.projects().locations().keyRings().list(parent=f"projects/{project_id}/locations/-")

GCP rejects - as a wildcard for KMS location listing — every project produces a single NOT_ASSESSED finding (HttpError 404: "The request concerns location '-' but was..."), masking all KMS keys regardless of rotation state. Verified against a project with 3 KMS keys (2 non-compliant, 1 compliant) — pre-fix scanner output:

[gcp-kms-key-rotation] Unable to list KMS key rings
  description: API call failed: <HttpError 404 ... locations/- ...>

Fix in commit afd65cf: enumerate KMS-supported locations first via kms.projects().locations().list(name="projects/X"), then list keyRings per location. Per-location failures don't abort the whole check. Same project after the fix correctly emits 1 FAIL with the 2 offenders enumerated.

KMS unit test mocks updated for the new API call shape — the chain setup needs an extra mock entry:

kms.projects.return_value.locations.return_value.list.return_value.execute.return_value = {
    "locations": [{"locationId": "global"}]
}

Bug 2 — Subnet checks emit per-region aggregates, not per-subnet

check_subnet_flow_logs_enabled and check_private_google_access_enabled (src/shasta/gcp/networking.py:374 and :460) collect non-compliant subnet names into a missing list, then emit one aggregate finding per region with all the offending names jammed into the description string. Side effects:

  1. Compliant subnets in regions that also have non-compliant subnets get no PASS finding — they're invisible.
  2. The aggregate finding's resource_id is projects/X/regions/Y/subnetworks (a query path, not a real resource) — risk register entries and remediation tracking can't link to the actual offender.
  3. The auto-created default VPC populates a subnet in every GCP region, so a clean test project produces ~40 region-aggregate FAILs that hide the COMPLIANT subnet a user explicitly created.

Fix in commit e7eea3c: emit one finding per subnet, with resource_id derived from the subnet's selfLink. Pre-fix output had 42 FAIL × 0 PASS for our test env; post-fix has 43 FAIL × 1 PASS (the COMPLIANT subnet now visible).

Bug 3 — Token-creator check ignores SA holders at project scope

check_iam_service_account_token_creator at src/shasta/gcp/iam.py:510:

non_sa = [m for m in members if not m.startswith("serviceAccount:")]
if non_sa:
    offenders.append({"role": role, "members": non_sa})

The filter excludes service-account holders from offenders. So a project-level binding of roles/iam.serviceAccountTokenCreator granted to a service account silently passes — even though compromise of that SA produces the same blast radius as a human holder (impersonation of every SA in the project). Verified: granted the role to shasta-bad-tokencreator-sa@... at project scope → gcloud projects get-iam-policy confirms binding exists → check returned PASS.

This is technically defensible as a strict CIS GCP 1.6 reading ("IAM Users... at Project Level"), but:

  • The check's own docstring says the role "should exist only in tightly scoped resource-level bindings, not at the project level" — full stop, no carve-out for SA holders.
  • Industry tools (Prowler, ScoutSuite, CloudSploit) flag any holder type at project scope.
  • Google's least-privilege docs recommend resource-level scoping regardless of principal type.

Fix in commit 79076fc: drop the non_sa filter, flag any project-level binding. Updated docstring + finding text to make the broad scope explicit. If the strict CIS reading is preferred, that's a reasonable counter-argument — happy to flip it back, but I'd suggest also adding a separate check_iam_token_creator_users_only so the granular interpretation isn't lost.

Things working well in PR #20

  • _run_gcp_checks failure handling (scanner.py:451-469) — wrapping each runner in try/except + emitting gcp-<domain>-scan-error NOT_ASSESSED is exactly the right pattern (engineering principle #6: "treat empty results different from errors"). Caught a real case in testing where Cloud Run me-central2 returns 403 — surfaced cleanly without crashing the scan.
  • cis_gcp_controls field on Finding model — populated consistently across all the GCP modules, matches the AWS/Azure pattern.
  • 130 unit tests covering check logic + 17 Terraform template renders. Solid test density.

Coverage gap (separate from the bugs above)

Three checks have no unit-test coverage at all — check_iam_no_allusers_access, check_iam_service_account_token_creator, check_iam_workload_identity_preferred. Bug #3 above survived because nothing tested it. Worth a follow-up commit before merge.

Validation summary

130/130 GCP unit tests green after all three fixes (one mock update needed for KMS). Live scan against the test project produces 125 findings across 34 distinct check IDs — BAD fixtures FAIL, COMPLIANT fixtures PASS, error paths emit NOT_ASSESSED instead of misleading PASS results.

Happy to discuss any of the fixes or open separate issues for the coverage gap. Thanks again for the contribution.

Debian and others added 5 commits May 7, 2026 08:32
Surface GCP runner and per-resource API failures as NOT_ASSESSED findings instead of silent drops or false passes. Add GCP provider labels, CIS GCP report/dashboard rendering, project-number parsing fixes, docs, and regression tests.
Bug 1 — KMS location wildcard returns 404
  encryption.py: replace locations/- wildcard with explicit location
  enumeration via projects().locations().list(). Per-location keyRings
  failures continue rather than aborting the whole check.

Bug 2 — Subnet checks emit per-region aggregates, not per-subnet
  networking.py: check_subnet_flow_logs_enabled and
  check_private_google_access_enabled now emit one Finding per subnet
  (PASS or FAIL) with resource_id from selfLink. Compliant subnets in
  mixed regions are now visible; resource_id resolves to the actual
  offending resource rather than a query path.

Bug 3 — Token-creator check ignores SA holders at project scope
  iam.py: drop the non_sa filter — any project-level binding of
  serviceAccountTokenCreator/serviceAccountUser now fails regardless
  of principal type (SA holders have the same blast radius as users).

Tests: 130 → 142 GCP tests. Added TestCheckServiceAccountTokenCreator
  (5 tests, including the SA-at-project-scope regression) and
  TestCheckWorkloadIdentityPreferred (4 tests) closing the coverage gap
  flagged in the review. KMS mocks updated for the two-step API shape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebasing onto main (post voice-console PR transilienceai#21) brings in 74 new voice
tests, pushing the total from 720 to 814. The ±50 integrity tolerance
is exceeded at 94, so update the TRUST.md TL;DR bullet to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@oswin-mako oswin-mako force-pushed the fix/gcp-scan-reporting branch from ec68be4 to adafa55 Compare May 7, 2026 11:31

Copy link
Copy Markdown
Collaborator

Hey @oswin-mako — friendly nudge on this one. The review above flags three scanner bugs (KMS location wildcard 404, per-region subnet aggregation, and the SA filter on the token-creator check) plus a rebase against current main (PR #21 / voice-console touched README/TRUST/CHANGELOG). Reference fixes are staged on the review/pr-20-gcp-fixes branch — feel free to cherry-pick or write your own equivalents, whichever you prefer. Happy to discuss any of them. Thanks again for the contribution.


Generated by Claude Code

@oswin-mako

Copy link
Copy Markdown
Author

Let me know if we need to change anything else

@kkmookhey kkmookhey merged commit 5e100c6 into transilienceai:main May 14, 2026
kkmookhey added a commit that referenced this pull request May 15, 2026
* feat(gcp): wire GCP into config and connect/scan skills

The GCP check modules and scanner integration already existed, but the
config layer and skill-driven UX did not — there was no way to drive a
GCP scan through the normal flow.

- add gcp_project_id / gcp_region to ShastaConfig (with project-id
  format validator) and the load_config defaults
- add get_gcp_client() convenience constructor, mirroring
  get_aws_client() / get_azure_client()
- add /connect-gcp skill mirroring /connect-azure (ADC auth via gcloud,
  [gcp] extra install, credential validation)
- collapse the three near-duplicate per-cloud blocks in /scan into one
  parameterized block that scans whichever clients are configured

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: refresh TRUST.md test counts and suite breakdown after GCP merge

The GCP integration (PR #20) added ~306 tests but left TRUST.md's
internal test counts stale and self-contradictory (TL;DR said 814,
Layer 1 said 519/684, run-output said 537). Sync every count to the
real collected total of 851, add tests/test_gcp and tests/voice rows
to the Layer 1 breakdown, and correct the integrity-test list (it
named three Whitney tests deleted in the 2026-04-13 split).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add GCP to CLAUDE.md project map and conventions

PR #20 added src/shasta/gcp/ but left CLAUDE.md describing an
AWS+Azure-only platform. Add GCP to the project description, tech
stack, project layout, install extras, client-session convention,
the multi-region/subscription principle (now multi-project via
GCPClient.for_project), and the required smoke-test list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: gitignore .bin/ local CLI binaries

Each developer can bootstrap uv/uvx into ./.bin/ without committing
the 48MB binary blobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants